vlwkaos' digital garden

Interior mutability and Cell

기존 데이터 참조 방식은:

  • & immutable / shared reference (read)
  • &mut mutable reference / unique reference (read/write)

데이터를 조작하는 또 다른 방법 Cell

  • mut을 쓰지 않고 데이터를 변환할 수 있음 (interior mutability)
  • !Sync 쓰레드간 싱크를 맞추지 않음.
  • 내부 값이 shared reference를 통해서도 바뀔 수 있을 때 interior mutability를 가진다고 한다.
  • 이는 기존의 법칙을 위반하는 행위이지만 좀 더 선택지를 제공하기 위해 만들어진 기능
    • UnsafeCell<T>는 러스트에서 유일하게 mutable하지 않은 인스턴스의 값을 변형할 수 있는 방법임
    • Cell<T>, RefCell<T>모두 내부적으로 UnsafeCell<T>를 이용함.
let mut x = 9; // 11을 원할때 아래는 thread safe하지 않음

thread 1 {
    x+=1;
}

thread 2 {
    x+=1;
}

또 다른 방법:

- `RefCell`
- `Mutex`
- `RwLock`
use std::cell::Cell;

#[derive(Debug)]
struct PhoneModel {
    company_name: String,
    model_name: String,
    screen_size: f32,
    memory: usize,
    date_issued: u32,
    on_sale: Cell<bool>
} 

fn main() {

    // struct의 instance가 가진 값 중 하나만 변경하고 싶을 때
    // instance를 mut로 선언하고 싶지 않음
    // 변경을 원하는 field에 Cell 타입을 지정한다.
    let super_phone_3000 = PhoneModel {
        company_name: "ABC".to_string(),
        model_name: "ABC S".to_string(),
        screen_size: 5.0,
        memory: 4_000_000,
        date_issued: 2020,
        on_sale: Cell::new(true)
    };
    
    println!("{super_phone_3000:?}");
    super_phone_3000.on_sale.set(false);
    println!("{super_phone_3000:?}");
}

결과값:

PhoneModel { company_name: "ABC", model_name: "ABC S", screen_size: 5.0, memory: 4000000, date_issued: 2020, on_sale: Cell { value: true } }

PhoneModel { company_name: "ABC", model_name: "ABC S", screen_size: 5.0, memory: 4000000, date_issued: 2020, on_sale: Cell { value: false } }
use std::cell::Cell;

fn main() {
    let c = Cell:new(String::from("I am a String"));
    c.set(String::from("I am a String?"));
    let s = c.get(); // copy type일때만 사용할 수 있음, returns copy of contained value
    let s = c.into_inner(); // unwrap Cell
}

RefCell

  • RefCell은 runtime checked borrowing rules
    • Cell은 mut이 아니라 값을 빌려사용할 수 없다?
  • &mut는 컴파일 시점에 체크
use std::cell::RefCell;

// panic은 unwind stack
#[derive(Debug)]
struct User {
    id: u32,
    year: u32,
    name: String,
    active: RefCell<bool>
}


fn main() {
    let u = User {
        id: 1,
        year: 2020,
        name: "U1".to_string(),
        active: RefCell::new(true)
    };
    
    let f_ref = u.active.borrow_mut();
    let s_ref = u.active.borrow_mut(); // panic

}
thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:22:26
use std::cell::RefCell;

// panic은 unwind stack
#[derive(Debug)]
struct User {
    id: u32,
    year: u32,
    name: String,
    active: RefCell<bool>
}


fn main() {
    let u = User {
        id: 1,
        year: 2020,
        name: "U1".to_string(),
        active: RefCell::new(true)
    };
    
    println!("{u:?}"); // true
    let mut f_ref = u.active.borrow_mut();
    
    println!("{u:?}"); // borrowed
    *f_ref = false; 
    
    println!("{u:?}"); // borrowed
    drop(f_ref);
    
    println!("{u:?}"); // false

}
  • try_borrow_mut 로 안전하게 참조 가져올 수 있음
  • 안가져가고 바로 쓰기
use std::cell::RefCell;

fn main() {
    let rc = RefCell::new(String::from("I am a String"));
    println!("{rc:?}");
    *rc.borrow_mut() = String::from("Change it"); // RefMut type
    println!("{rc:?}"); // 바로 변경하면 borrowed 대상이 없어서 바로 반환됨
    
    match rc.try_borrow_mut() { // 역시 반환 대상없음
        Ok(mut r) => *r = String::from("Ok"),
        Err(e) => println!("Error: {e}")
    };
    
    println!("{rc:?}"); // Ok
    
    let block = rc.borrow_mut(); // 가져감
    match rc.try_borrow_mut() {
        Ok(mut r) => *r = String::from("Ok"), 
        Err(e) => println!("Error: {e}") // Error: already borrowed
    };
 
    println!("{rc:?}"); // borrowed runtime checked
}

BorrowFlag R/W 상태를 runtime에서 확인하기 위한 플래그 Cell 에서 사용한다.

왜 interior mutability를 사용하나

외부 모듈을 사용할 때 trait가 있는 경우

use std::cell::{Cell, RefCell};

// 외부 모듈이 이런 trait을 구현하도록 만들어져있을 때
// &mut self가 아니라서 값을 변형할 수 없다면
trait XTrait {
    fn cool_fun(&self);
}

// 이렇게 struct에서 interior mutability를 부여한 뒤
#[derive(Debug)]
struct User {
    id: u32,
    count: Cell<u32>
}


impl XTrait for User {
    // 이런식으로 값을 변형할 수 있다.
    fn cool_fun(&self) {
        let count = self.count.get();
        self.count.set(count+1);
        println!("Counting...");
    }
}


fn main() {
    let u = User {
        id: 123,
        count: Cell::new(0)
    };
    
    for _ in 0..20 {
        u.cool_fun();
    }
}

Reference Counter

러스트 초보자는 Rc를 도배할 수 있음. T에 대한 shared ownership을 heap에 저장하여 제공한다. (shared_ptr) Rc의 clone은 힙의 같은 위치를 가르키는 새로운 포인터를 만든다.

use std::rc::Rc;

fn ts(input: String) {
    
}

fn ts2(input: Rc<String>) {
    println!("{input}");
}

fn ts3(input: Rc<String>) {
    println!("{input}");
}

fn ts4(input: &String) {
    println!("{input}");
}

fn main() {
    let str = "Hello there".to_string();
    //ts0(str); // 함수로 소유가 넘어가서 끝남
    ts(str); // X
    
    let str2 = "Test2".to_string();
    ts(str2.clone()); // clone은 anti-type. 안쓰는게 좋다
    ts(str2);
    
    let str3 = Rc::new("Rc String".to_string());
    // 소유자가 하나 더 생긴다?
    ts2(Rc::clone(&str3)); // 메모리를 거의 쓰지 않음
    ts3(Rc::clone(&str3));

    let str4 = "SharedPtr".to_string();
    ts4(&str4);
    ts4(&str4);
    
    let rc = Rc::new(());
    // method-call syntax
    let rc2 = rc.clone();
    // Fully qualified syntax
    let rc3 = Rc::clone(&rc);
}

예시

도구는 주인에게 속한다. 여러 도구는 한 주인에게 속할 수 있다. unique ownership으로는 이를 구현할 수 없다.

use std::rc::Rc;

struct Owner {
    name: String,
    // ...other fields
}

struct Gadget {
    id: i32,
    // Gadget인스턴스가 여럿이라 할 때 
    // owner값을 동일한 Owner 인스턴스로 지정하려면 소유권 문제를 해결해야함. 
    owner: Rc<Owner>, 
    // ...other fields
}

fn main() {
    // 참조가 여럿인 Owner instance를 만든다.
    let gadget_owner: Rc<Owner> = Rc::new(
        Owner {
            name: "Gadget Man".to_string(),
        }
    );

    // Rc를 clone함으로써 참조의 개수만 늘려서 저렴하게 지정이 가능함
    let gadget1 = Gadget {
        id: 1,
        owner: Rc::clone(&gadget_owner),
    }; // 그냥 shared ref를 주면? 이 인스턴스가 점거를 하고 있나?
    let gadget2 = Gadget {
        id: 2,
        owner: Rc::clone(&gadget_owner),
    };

    // Rc owner instance를 메모리에서 해제
    drop(gadget_owner);

    // 여전히 출력할 수 있다. Rc만 drop이 된 상태라서 참조가 남아있음
    // `Rc<Owner>` 는 알아서 `Owner`로 deferenced된다
    println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name);
    println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name);

    // 이 시점에서 완전히 해제된다.
}
  • partial borrowing. field

🤔 lifetime과의 비교

struct Owner {
    name: String,
    // ...other fields
}

struct Gadget<'a> {
    id: i32,
    // lifetime 
    owner: &'a Owner, 
    // ...other fields
}

fn main() {
    let gadget_owner: Owner = Owner {
            name: String::from("TEST"),
        };
    
    let gadget1 = Gadget {
        id: 1,
        owner: &gadget_owner,
    }; 
     let gadget2 = Gadget {
        id: 2,
        owner: &gadget_owner,
    }; 

    println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name);
    println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name);
}

Rc를 써도 되지만, Rust의 장점인 zero-cost abstraction를 통한 빠른 속도는 lifetime과 reference를 이해했을 때 가능하다. C/C++와 같은 언어에서는 작성하기 엄두가 안나는 코드(메모리릭)를 러스트로 작성이 가능하다. rc와 같은 smart pointer는 runtime 비용이있다.

Rc를 왜 써야하나?


Rc는 strong_count

  • 참조가 1개라도 있을 경우 메모리 drop하지 않음

만약 위의 코드에서 Owner가 Gadget를 참조하게 되면 순환참조가 걸려서 메모리 누수가 발생한다. 이럴 때는 Weak pointer를 사용해야한다. Weak 참조

Weak 은 non-owning reference이다. 값을 실제로 초기화 하지 않고 메모리 공간만 할당해놓는다. (값을 보장할 수 없고(덮어씌워질 수 있다?) upgrade로 메모리에 값이 남아있는지 확인 가능하다.

러스트는 태생적으로 순환참조가 어렵도록 설계되었다. 순환참조를 만드려면 둘중하나는 mutable이어야한다. Rc를 사용하면 애초에 shared reference만 주기 때문에 mutable하지 않다. 이걸 정 해야한다면 interior mutability를 활용한다. (RefCell)

Rc and RefCell 예제

use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let a = Rc::new(RefCell::new("Interior Mutability".to_string()));
    // Rc는 알아서 deref되니까 
    *a.borrow_mut() = "Now".to_string();
    println!("{}", a.borrow_mut()); // Now
}

Referred in

Rust - interior mutability, cell, RefCell, Reference counter